/**
 * 
 */
package com.faxsun.core.service.impl;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import mblog.core.lang.Consts;
import mblog.core.persist.dao.AttachDao;
import mblog.core.persist.dao.PostDao;
import mblog.core.persist.dao.TagDao;
import mblog.core.persist.dao.UserDao;
import mblog.core.pojos.Attach;
import mblog.core.pojos.Post;
import mblog.core.pojos.Tag;
import mblog.core.utils.BeanMapUtils;
import mtons.modules.pojos.Page;
import mtons.modules.pojos.UserContextHolder;
import mtons.modules.pojos.UserProfile;

import org.apache.commons.lang.StringUtils;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.search.highlight.Highlighter;
import org.apache.lucene.search.highlight.InvalidTokenOffsetsException;
import org.apache.lucene.search.highlight.QueryScorer;
import org.apache.lucene.search.highlight.SimpleHTMLFormatter;
import org.hibernate.search.FullTextQuery;
import org.hibernate.search.FullTextSession;
import org.hibernate.search.Search;
import org.hibernate.search.SearchFactory;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import com.faxsun.core.persist.entity.FollowListPO;
import com.faxsun.core.persist.entity.PostPO;
import com.faxsun.core.persist.entity.TagPO;
import com.faxsun.core.pojos.enums.PostType;
import com.faxsun.core.pojos.enums.TagType;
import com.faxsun.core.service.AttachService;
import com.faxsun.core.service.FollowListService;
import com.faxsun.core.service.PostService;
import com.faxsun.core.service.TagService;
import com.faxsun.core.utils.PreviewHtmlUtils;
import com.faxsun.helper.HTMLHelper;
import com.faxsun.web.pojos.FSUserProfile;

/**
 * @author langhsu
 *
 */
public class PostServiceImpl implements PostService {
	@Autowired
	private PostDao postDao;
	@Autowired
	private AttachDao attachDao;
	@Autowired
	private UserDao userDao;
	@Autowired
	private AttachService attachService;
	@Autowired
	private TagService tagService;
	@Autowired
	private TagDao tagDao;
	@Autowired
	private FollowListService flService;
    
    private final static String CACHE = "postsCaches";

	@Override
	@Transactional(readOnly = true)
	public void paging(Page page, PostType type) {
		this.pagingByStatus(page, type, 1);
	}

	@Override
	@Transactional(readOnly = true)
	public void pagingByUserId(Page page, long userId) {
		this.pagingByUserId(page, userId, 1);
	}

	@Override
	@Transactional(readOnly = true)
	public void pagingByUserId(Page page, long userId, int status) {
		List<PostPO> list = postDao.pagingByUserId(page, userId, status);
		List<Post> rets = new ArrayList<Post>();
		for (PostPO po : list) {
			rets.add(BeanMapUtils.copy(po, 0));
		}
		page.setResults(rets);
	}

	@Override
	@Transactional(readOnly = true)
	public void pagingByUserIdWithoutDeleted(Page page, long userId) {
		List<PostPO> list = postDao.pagingByUserId(page, userId, -1);
		List<Post> rets = new ArrayList<Post>();
		for (PostPO po : list) {
			if (po.getStatus() < 2) {
				rets.add(BeanMapUtils.copy(po, 0));
			}
		}
		page.setResults(rets);
	}

	@Override
	@Transactional(readOnly = true)
	@SuppressWarnings("unchecked")
	public List<Post> search(Page page, String q) throws InterruptedException,
			IOException, InvalidTokenOffsetsException {
		FullTextSession fullTextSession = Search.getFullTextSession(postDao
				.getSession());
		// fullTextSession.createIndexer().startAndWait();
		SearchFactory sf = fullTextSession.getSearchFactory();
		QueryBuilder qb = sf.buildQueryBuilder().forEntity(PostPO.class).get();
		org.apache.lucene.search.Query luceneQuery = qb.keyword()
				.onFields("title", "summary", "tags").matching(q).createQuery();
		FullTextQuery query = fullTextSession.createFullTextQuery(luceneQuery);
		query.setFirstResult(page.getFirstResult());
		query.setMaxResults(page.getMaxResults());

		StandardAnalyzer standardAnalyzer = new StandardAnalyzer();
		SimpleHTMLFormatter formatter = new SimpleHTMLFormatter(
				"<span style='color:red;'>", "</span>");
		QueryScorer queryScorer = new QueryScorer(luceneQuery);
		Highlighter highlighter = new Highlighter(formatter, queryScorer);

		List<PostPO> list = query.list();
		int resultSize = query.getResultSize();

		List<Post> rets = new ArrayList<Post>();
		for (PostPO po : list) {
			Post m = BeanMapUtils.copy(po, 0);
			String title = highlighter.getBestFragment(standardAnalyzer,
					"title", m.getTitle());
			String summary = highlighter.getBestFragment(standardAnalyzer,
					"summary", m.getSummary());
			String tags = highlighter.getBestFragment(standardAnalyzer, "tags",
					m.getTags());
			if (StringUtils.isNotEmpty(title)) {
				m.setTitle(title);
			}
			if (StringUtils.isNotEmpty(summary)) {
				m.setSummary(summary);
			}
			if (StringUtils.isNotEmpty(tags)) {
				m.setTags(tags);
			}
			rets.add(m);
		}
		page.setTotalCount(resultSize);
		page.setResults(rets);
		return rets;
	}

	@Override
	@Transactional(readOnly = true)
	@SuppressWarnings("unchecked")
	public List<Post> searchByTag(Page page, String tag)
			throws InterruptedException, IOException,
			InvalidTokenOffsetsException {
		FullTextSession fullTextSession = Search.getFullTextSession(postDao
				.getSession());
		SearchFactory sf = fullTextSession.getSearchFactory();
		QueryBuilder qb = sf.buildQueryBuilder().forEntity(PostPO.class).get();
		org.apache.lucene.search.Query luceneQuery = qb.keyword()
				.onFields("tags").matching(tag).createQuery();
		FullTextQuery query = fullTextSession.createFullTextQuery(luceneQuery);
		query.setFirstResult(page.getFirstResult());
		query.setMaxResults(page.getMaxResults());

		List<PostPO> list = query.list();
		int resultSize = query.getResultSize();

		List<Post> rets = new ArrayList<Post>();
		for (PostPO po : list) {
			Post m = BeanMapUtils.copy(po, 0);
			rets.add(m);
		}
		page.setTotalCount(resultSize);
		page.setResults(rets);
		return rets;
	}

	@Override
	@Transactional(readOnly = true)
	public List<Post> findRecents(int maxResutls, long ignoreUserId) {
		List<PostPO> list = postDao.findRecents(maxResutls, ignoreUserId);
		List<Post> rets = new ArrayList<Post>();
		for (PostPO po : list) {
			rets.add(BeanMapUtils.copy(po, 0));
		}
		return rets;
	}

	@Override
	@Transactional(readOnly = true)
	public List<Post> findHots(int maxResutls, long ignoreUserId) {
		List<PostPO> list = postDao.findHots(maxResutls, ignoreUserId);
		List<Post> rets = new ArrayList<Post>();
		for (PostPO po : list) {
			rets.add(BeanMapUtils.copy(po, 0));
		}
		return rets;
	}

	@Override
	@Transactional(readOnly = true)
	public Map<Long, Post> findByIds(Set<Long> ids) {
		List<PostPO> list = postDao.findByIds(ids);
		Map<Long, Post> rets = new HashMap<Long, Post>();
		for (PostPO po : list) {
			rets.put(po.getId(), BeanMapUtils.copy(po, 0));
		}
		return rets;
	}

	@Override
	@Transactional
	public void post(Post post) {
		// FIXME: 增加事务自动重试，可能由于不同人输入同一个tag导致保存失败
		PostPO po = postDao.get(post.getId());
		if (po != null) {
			po.setUpdated(new Date());
		} else {
			po = new PostPO();
			UserProfile up = UserContextHolder.getUserProfile();

			// FIXME: use up instead of constant 1L
			po.setAuthor(userDao.get(up.getId()));
			// po.setAuthor(userDao.get(1L));
			po.setCreated(new Date());
			// po.setStatus(EntityStatus.ENABLED);

			// content
			po.setType(post.getType());
		}
		po.setStatus(post.getStatus());
		po.setTitle(post.getTitle());
		po.setContent(post.getContent());
		po.setSummary(trimSummary(post.getContent())); // summary handle
		po.setTags(post.getTags());

		// FIXME:优化tag的处理方式(?)
		processTags(po, post);
		postDao.saveOrUpdate(po);
		post.setId(po.getId());

		List<Attach> dbAttaches = attachService.list(po.getId());

		if (post.getSnapshot() != null
				&& !StringUtils.isEmpty(post.getSnapshot().getOriginal())) {
			// 界面上设置了封面图片

			Attach attach = post.getSnapshot();
			int index = dbAttaches.indexOf(attach);
			Attach dbAttach = null;
			if (index >= 0) {
				dbAttach = dbAttaches.get(index);
			}
			if (index < 0
					|| (dbAttach != null && dbAttach.getToId() != po.getId())) {// 封面图片不在数据库中或图片所属的文章id不同
				// 封面图片未存储过
				attach.setToId(po.getId());
				long id = attachService.add(attach);
				po.setSnapshot(attachDao.get(id));
			}
		}
		processImages(po, dbAttaches);
		// tag handle
		/*
		 * if (StringUtils.isNotBlank(post.getTags())) { List<Tag> tags = new
		 * ArrayList<Tag>(); String[] ts = StringUtils.split(post.getTags(),
		 * Consts.SEPARATOR);
		 * 
		 * for (String t : ts) { Tag tag = new Tag(); tag.setName(t);
		 * tag.setLastPostId(po.getId()); tag.setPosts(1); tags.add(tag); }
		 * 
		 * tagService.batchPost(tags); }
		 */
	}

	private void processTags(PostPO po, Post p) {
		Set<TagPO> relatedTags = po.getTagSets();
		if(relatedTags==null){
			relatedTags=new HashSet<TagPO>();
		}
		Set<TagPO> tagsPO = new HashSet<TagPO>();

		if (StringUtils.isNotBlank(po.getTags())) {
			String[] ts = StringUtils.split(po.getTags(), Consts.SEPARATOR);

			for (String t : ts) {
				TagPO tagPO = tagDao.getByName(t);
				// tag 不存在，则新建，此处可能会导致数据库事务失败
				// tag名称在数据库中唯一，同一个tag会导致违反唯一性约束
				if (tagPO == null) {
					tagPO = new TagPO();
					tagPO.setName(t);
					tagPO.setLastPostId(po.getId());
					tagPO.setPosts(0);
					tagPO.setEnumType(TagType.NORMAL);
					tagDao.save(tagPO);
				} else {
				}
				tagsPO.add(tagPO);
			}

			// 处理产品标签
			if (p.getProductTag() != null && p.getProductTag().getId() > 0L) {
				TagPO productTagPO = tagDao.get(p.getProductTag().getId());
				tagsPO.add(productTagPO);
				po.setDefaultTag(productTagPO);
			}

			/*
			 * TagPO categoryTagPO = tagDao.get(p.getDefaultCategoryId());
			 * tagsPO.add(categoryTagPO);
			 */

		}

		// 删除的tag posts数目-1
		Set<TagPO> toBeDeleted = new HashSet<TagPO>(relatedTags);
		toBeDeleted.removeAll(tagsPO);
		for (TagPO decreaseTag : toBeDeleted) {
			decreaseTag.setPosts(decreaseTag.getPosts() - 1);
			tagDao.save(decreaseTag);
		}

		// 新增加的tag, posts数目＋1
		Set<TagPO> toBeAdded = new HashSet<TagPO>(tagsPO);
		toBeAdded.removeAll(relatedTags);
		for (TagPO increaseTag : toBeAdded) {
			increaseTag.setPosts(increaseTag.getPosts() + 1);
			tagDao.save(increaseTag);
		}

		po.setTagSets(tagsPO);
	}

	@Transactional
	private void processImages(PostPO post, List<Attach> dbAttachesSet) {
		String content = post.getContent();
		Set<String> imageStrings = HTMLHelper.getImgs(content);
		long toId = post.getId();
		boolean isFirst = true;

		for (String imageString : imageStrings) {
			// FIXME:图片后缀及格式验证抽取为方法
			if (imageString.endsWith(".jpg") || imageString.endsWith(".jpeg")
					|| imageString.endsWith(".png")
					|| imageString.endsWith(".gif")) {
				Attach attach = new Attach();
				attach.setToId(toId);
				attach.setOriginal(imageString);

				int index = dbAttachesSet.indexOf(attach);
				Attach dbAttach = null;

				if (index >= 0) {
					dbAttach = dbAttachesSet.get(index);
				}

				if (dbAttach != null && dbAttach.getToId() == toId) {
					// 同一篇文章的同一个图片，复用已有图片
					continue;
				}

				long id = attachService.add(attach);
				if (isFirst) {
					// 文章中第一个图片
					if ((post.getSnapshot() == null // 当前文章未设置封面
							|| post.getSnapshot().getId() <= 0 // 或当前文章封面id非法
					|| (post.getSnapshot().getOriginal().equals("/faxsun-info")))) {
						isFirst = false;
						post.setSnapshot(attachDao.get(id));

					}
				}

			}
		}
	}

	@Override
	@Transactional
	public Post get(long id) {
		PostPO po = postDao.get(id);
		Post d = null;
		if (po != null) {
			d = BeanMapUtils.copy(po, 1);

			List<Attach> albs = attachService.list(d.getId());
			d.setAlbums(albs);

			List<Tag> tagList = new ArrayList<Tag>();
			for (TagPO tagPO : po.getTagSets()) {
				Tag tag = BeanMapUtils.copy(tagPO);
				tagList.add(tag);
			}
			d.setTagList(tagList);
		}

		return d;
	}

	@Override
	@Transactional
	public void delete(long id) {
		PostPO po = postDao.get(id);
		if (po != null) {
			// attachService.deleteByToId(id);
			// postDao.delete(po);
			this.delete(po);

		}
	}

	@Override
	@Transactional
	public void delete(long id, long authorId) {
		PostPO po = postDao.get(id);
		if (po != null) {
			Assert.isTrue(po.getAuthor().getId() == authorId, "认证失败");
			// attachService.deleteByToId(id);
			// postDao.delete(po);
			// FIXME: use constants or enums instead of magic number
			this.delete(po);
		}
	}

	@Override
	@Transactional
	public void identityViews(long id) {
		PostPO po = postDao.get(id);
		if (po != null) {
			po.setViews(po.getViews() + Consts.IDENTITY_STEP);
		}
	}

	@Override
	@Transactional
	public void identityHearts(long id) {
		PostPO po = postDao.get(id);
		if (po != null) {
			po.setHearts(po.getHearts() + Consts.IDENTITY_STEP);
		}
	}

	@Override
	@Transactional
	public void identityComments(long id) {
		PostPO po = postDao.get(id);
		if (po != null) {
			po.setComments(po.getComments() + Consts.IDENTITY_STEP);
		}
	}

	@Override
	@Transactional
	public void identityComments(long id, int step) {
		PostPO po = postDao.get(id);
		if (po != null) {
			po.setComments(po.getComments() + step);
		}
	}

	@Override
	@Transactional
	public int countRecentNews(int maxResutls, long ignoreUserId) {
		List<PostPO> list = postDao.findRecents(maxResutls, ignoreUserId,
				PostType.INFO.getValue());
		int ret = (list != null && list.size() > 0) ? list.size() : 0;
		return ret;
	}

	@Override
	@Transactional
	public int countRecentNewsForTag(long ignoreUserId, List<String> tags) {
		int ret = postDao.countRecentByTags(ignoreUserId,
				PostType.INFO.getValue(), tags);
		return ret;
	}

	/**
	 * 截取文章内容
	 * 
	 * @param text
	 * @return
	 */
	private String trimSummary(String text) {
		return PreviewHtmlUtils.truncateHTML(text, 126);
	}

	/**
	 * 按状态查找文章
	 */
	@Override
	@Transactional(readOnly = true)
	public void pagingByStatus(Page page, PostType type, int status) {

		List<PostPO> list = postDao.pageingByStatus(page, type, status);
		List<Post> rets = transform(list);
		page.setResults(rets);
	}

	@Override
	@Transactional(readOnly = true)
    @Cacheable(value = CACHE, key = "'findRecentPosts?pageNo'+#page.pageNo+'&maxResults='+#page.maxResults+'&postType='+#type.value")
	public List<Post> findRecentPosts(Page page, PostType type) {

		List<PostPO> list = postDao.findRecents(page.getMaxResults(), 0L,
				type.getValue());
		List<Post> rets = transform(list);
		page.setResults(rets);
		return rets;
	}

	/**
	 * 将实体类转换为页面显示类，并统计关注数,当前登录用户是否关注
	 * 
	 * @param list
	 * @return
	 */
	private List<Post> transform(List<PostPO> list) {
		List<Post> rets = new ArrayList<Post>();
		for (PostPO po : list) {
			Post p = BeanMapUtils.copy(po, 0);

			int followed_count = flService.countAllMyFollowers(po.getId(), 2);
			p.setFollowsCounts(followed_count);

			UserProfile up = UserContextHolder.getUserProfile();
			String poFollowed = "false";
			if (up != null) {
				FollowListPO fl = flService.findFollow(po.getId(), up.getId(),
						2);
				poFollowed = fl == null ? "false"
						: (fl.getStatus() == 0 ? "true" : "false");
				p.setFollowerId(up.getId());
			} else {
				p.setFollowerId(1);
			}
			p.setIsUserFollowed(poFollowed);

			rets.add(p);
		}
		return rets;
	}

	@Transactional
	private void delete(PostPO po) {
		if (po != null && (isAuthorized(po))) {
			po.setStatus(2);// deleted
			Set<TagPO> tags = po.getTagSets();
			for (TagPO tag : tags) {
				tag.setPosts(tag.getPosts() - 1);
				tagDao.save(tag);
			}
			postDao.save(po);
		}
	}

	/**
	 * 恢复已删除的文章为审核中
	 * 
	 * @param po
	 */
	@Transactional
	private void recover(PostPO po) {
		if (po != null && isAuthorized(po)) {
			po.setStatus(0);// 审核中
			Set<TagPO> tags = po.getTagSets();
			for (TagPO tag : tags) {
				tag.setPosts(tag.getPosts() + 1);
				tagDao.save(tag);
			}
			postDao.save(po);
		}
	}

	private boolean isAuthorized(PostPO po) {
		FSUserProfile up = (FSUserProfile) UserContextHolder.getUserProfile();
		boolean isAuthorized = up.getRoleLevel() == 1;
		isAuthorized = isAuthorized || (up.getId() == po.getAuthor().getId());
		return isAuthorized;
	}

	@Override
	@Transactional
	public void recover(long id) {
		PostPO po = postDao.get(id);
		this.recover(po);
	}

}
